package com.korustar.autumnal.codemodel;

import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;

import com.korustar.autumnal.reflect.ClassLoaderUtil;
import com.korustar.autumnal.reflect.ConstructorReflection;
import com.korustar.autumnal.reflect.StaticMethodReflection;
import com.korustar.autumnal.spring.BeanDefinitionUtil;
import com.sun.codemodel.JBlock;
import com.sun.codemodel.JClass;
import com.sun.codemodel.JExpr;
import com.sun.codemodel.JExpression;
import com.sun.codemodel.JInvocation;

/**
 * Bean construction builder
 * 
 * <p>Responsible for bean construction
 * 
 * @author amir
 *
 */
public class CodeModelBeanConstructionBuilder {
	
	private static Logger LOGGER = LoggerFactory.getLogger(CodeModelBeanConstructionBuilder.class);
	
	private CodeModelParameterBuilder parameterBuilder;

	public JExpression invokeConstructor(BeanDefinition beanDefinition, CodeModelContainerModel model, JBlock currentBlock) {
		if(BeanDefinitionUtil.SPRINGFRAMEWORK_PROPERTY_PLACEHOLDER_CONFIGURER.equals(beanDefinition.getBeanClassName())) {
			return invokePropertiesConstructor(model); 
		} else if(BeanDefinitionUtil.SPRINGFRAMEWORK_FIELD_RETRIEVING_FACTORY_BEAN.equals(beanDefinition.getBeanClassName())) {
			return makeConstant(beanDefinition);
		} else if(beanDefinition.getFactoryMethodName()!=null) {
			return invokeFactoryMethod(beanDefinition, model, currentBlock);
		} else {
			return invokeStandardConstructor(beanDefinition, model, currentBlock);
		}
	}
	
	private JExpression invokeFactoryMethod(BeanDefinition beanDefinition, CodeModelContainerModel model,
			JBlock currentBlock) {
		//String factoryBeanName = beanDefinition.getFactoryBeanName();
		String factoryMethodName = beanDefinition.getFactoryMethodName();
		JClass ref = model.getCodeModel().ref( beanDefinition.getBeanClassName() );
		JInvocation constructor = ref.staticInvoke(factoryMethodName); 
		return addConstructorArgs(constructor, beanDefinition, model, currentBlock);
	}

	private JExpression invokePropertiesConstructor(CodeModelContainerModel model) {
		JClass ref = model.getCodeModel().ref(Properties.class);
		return JExpr._new(ref);
	}

	private JExpression makeConstant(BeanDefinition beanDefinition) {
		MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
		List<PropertyValue> propertyValueList = propertyValues.getPropertyValueList();
		for(PropertyValue propertyValue : propertyValueList) {
			String name = propertyValue.getName();
			//NOTE: apply propertyPlaceholder on value ...
			Object value = propertyValue.getValue();
			if(BeanDefinitionUtil.SPRINGFRAMEWORK_FIELD_RETRIEVING_FACTORY_BEAN_STATIC_FIELD.equals(name)) {
				LOGGER.info("Making a 'static field' ... {}", value);
				return JExpr.direct((String)value);
			}
		}
		LOGGER.error("unexpected! no staticField property on constant");
		return JExpr._null();
	}

	private JExpression invokeStandardConstructor(BeanDefinition beanDefinition, CodeModelContainerModel model, JBlock currentBlock) {
		JInvocation constructor = JExpr._new(model.getCodeModel().ref( beanDefinition.getBeanClassName() ) );
		return addConstructorArgs(constructor, beanDefinition, model, currentBlock);
	}
	
	private JExpression addConstructorArgs(JInvocation constructor, BeanDefinition beanDefinition, CodeModelContainerModel model, JBlock currentBlock) {
		ConstructorArgumentValues constructorArgumentValues = beanDefinition.getConstructorArgumentValues();
		
		int argumentCount = constructorArgumentValues.getArgumentCount();
		
		if(argumentCount>0) {
			Class<?> beanTypeFromClasspath = ClassLoaderUtil.getClassFromClasspath(beanDefinition.getBeanClassName());
			List<Class<?>> constructorArgTypes = null;
			if(beanTypeFromClasspath!=null) {
				if(beanDefinition.getFactoryMethodName()==null) {
					//try to get constructor args
					constructorArgTypes = ConstructorReflection.getConstructorArgTypes(beanTypeFromClasspath, argumentCount);
					LOGGER.info("constructor arg types from constructor reflection: {}({})", new Object[]{ beanDefinition.getBeanClassName(), constructorArgTypes } );
				} else {
					
					constructorArgTypes= StaticMethodReflection.getArgTypes(beanDefinition.getBeanClassName(), beanDefinition.getFactoryMethodName(), constructorArgumentValues.getArgumentCount());
					LOGGER.info("constructor arg types from static reflection: {}.{}({})", new Object[]{ beanDefinition.getBeanClassName(), beanDefinition.getFactoryMethodName(), constructorArgTypes } );
				}
			}
			
			//TODO: indexed argument values ...
			Map<Integer, ValueHolder> indexedArgumentValues = constructorArgumentValues.getIndexedArgumentValues();
			int done=0;
			for(Integer id : indexedArgumentValues.keySet()) {
				Class<?> expectedArgType= constructorArgTypes == null ? null : constructorArgTypes.get(id);
				ValueHolder valueHolder= indexedArgumentValues.get(id);
				JExpression exp = getConstructorArg(valueHolder, expectedArgType, model, currentBlock);
				constructor.arg(exp);
				done++;
			}
			//TODO - improve this strategy!!!
			List<ValueHolder> genericArgumentValues = constructorArgumentValues.getGenericArgumentValues();
			for(ValueHolder valueHolder : genericArgumentValues) {
				Class<?> expectedArgType= constructorArgTypes == null ? null : constructorArgTypes.get(done);
				JExpression exp = getConstructorArg(valueHolder, expectedArgType, model, currentBlock);
				constructor.arg(exp);
				done++;
			}
		}
		return constructor;
	}

	private JExpression getConstructorArg(ValueHolder valueHolder, Class<?> expectedType, CodeModelContainerModel model, JBlock currentBlock) {
		Object designatedValue= valueHolder.getValue();
		String designatedType = valueHolder.getType();
		LOGGER.info("generic constructor arg val: {}, type: {}, valueClass: {} ", new Object[] { valueHolder.getValue(), valueHolder.getType(), valueHolder.getValue().getClass() } );
		LOGGER.info("Expected type: {}, Class type: {}, value: {}", new Object[]{ expectedType, valueHolder.getValue().getClass(), valueHolder.getValue() } );
		//this.checkDefinitionRequired(designatedValue, model, currentBlock);
		return this.getParameterBuilder().buildParameter(designatedValue, new ParameterContext(model, currentBlock,
				expectedType, designatedType));
	}

	public void setParameterBuilder(CodeModelParameterBuilder parameterBuilder) {
		this.parameterBuilder = parameterBuilder;
	}

	public CodeModelParameterBuilder getParameterBuilder() {
		return this.parameterBuilder;
	}
}
